﻿using Red.Core.Network.PacketNetwork;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

namespace Red.Core.Network
{
    /// <summary>
    /// RawNamedPipeManager
    /// </summary>
    public class RawNamedPipeManager : IRawConnectionsManager
    {
        private class RawListener
        {
            public int AssignedID;
            public RawNamedPipe AssignedSocket;
            public IRawListenerInterface AssignedCallback;
            public bool RemoveRequest = false;
        }

        private class RawConnection
        {
            public int AssignedID;
            public int AssignedListenerID = -1;
            public RawNamedPipe AssignedSocket;
            public IRawConnectionInterface AssignedCallback;
            public bool RemoveRequest = false;

            public byte[] ReceiveBuffer = new byte[ 4096 ];
        }

        private class SendingContext
        {
            public RawConnection Connection;
            public AsyncSendFinishedEventHandler FinishedEventHandler;

            public SendingContext( RawConnection connectionParam, AsyncSendFinishedEventHandler finishedEventHandlerParam )
            {
                Connection = connectionParam;
                FinishedEventHandler = finishedEventHandlerParam;
            }
        }

        private IDAllocator _idAllocator = new IDAllocator();
        private ConcurrentDictionary<int, RawListener> _listeners = new ConcurrentDictionary<int, RawListener>();
        private ConcurrentDictionary<int, RawConnection> _connections = new ConcurrentDictionary<int, RawConnection>();

        public delegate void AsyncSendFinishedEventHandler( int connectionID );
        
        public int CreateListener( CommAddress localAddress, IRawListenerInterface listenerCallback )
        {
            NamedPipeAddress pipeAddr = localAddress as NamedPipeAddress;
            RawNamedPipe pipe = new RawNamedPipe( pipeAddr.PipeName, 128 );

            int id = _idAllocator.AllocateID();

            RawListener listener = new RawListener();
            listener.AssignedID = id;
            listener.AssignedSocket = pipe;
            listener.AssignedCallback = listenerCallback;

            listener.AssignedSocket.BeginAccept( new AsyncCallback( AcceptedCallback ), listener );
            _listeners.AddOrUpdate( id, listener, ( key, oldValue ) => listener );

            return id;
        }

        public void CloseListener( int listenerID )
        {
            RawListener listener;
            bool listenerFound = _listeners.TryRemove( listenerID, out listener );
            if ( listenerFound )
            {
                foreach ( KeyValuePair<int, RawConnection> pair in _connections )
                {
                    if ( pair.Value.AssignedListenerID == listenerID )
                    {
                        RawConnection removedConnection;
                        _connections.TryRemove( pair.Key, out removedConnection );
                        removedConnection.AssignedSocket.Close();
                    }
                }

                listener.AssignedSocket.Close();
            }
        }

        public async Task<int> CreateConnection( CommAddress address, IRawConnectionInterface callback )
        {
            NamedPipeAddress pipeAddr = address as NamedPipeAddress;
            int id = _idAllocator.AllocateID();

            RawNamedPipe pipe = new RawNamedPipe( pipeAddr.PipeName, 128 );

            bool connected = await pipe.Connect();

            RawConnection connection = new RawConnection();
            connection.AssignedID = id;
            connection.AssignedSocket = pipe;
            connection.AssignedCallback = callback;

            _connections.AddOrUpdate( id, connection, ( key, oldValue ) => connection );

            connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, new AsyncCallback( ReceiveCallback ), connection );

            if ( connection.ReceiveBuffer.Length < pipe.GetBufferSize() )
            {
                throw new InternalBufferOverflowException( "Pipe buffer can't be smaller than static receive buffer for connection manager." );
            }

            return connection.AssignedID;
        }

        public void CloseConnection( int connectionId )
        {
            RawConnection connection;
            bool connectionFound = _connections.TryRemove( connectionId, out connection );

            if ( connectionFound )
            {
                connection.AssignedSocket.Close();
            }
        }

        public void SendAsync( int connectionID, byte[] buffer )
        {
            SendAsync( connectionID, buffer, ( sendingConnectionID ) => { } );
        }

        public void SendAsync( int connectionID, byte[] buffer, AsyncSendFinishedEventHandler finishedEventHandler )
        {
            RawConnection connection;
            bool connectionFound = _connections.TryGetValue( connectionID, out connection );

            if ( connectionFound )
            {
                connection.AssignedSocket.BeginSend( buffer, new AsyncCallback( SendCallback ), new SendingContext( connection, finishedEventHandler ) );
            }
        }

        private void AcceptedCallback( IAsyncResult result )
        {
            RawListener listener = ( RawListener )result.AsyncState;

            int id = _idAllocator.AllocateID();

            IRawConnectionInterface connectionCallback;
            bool connectionAccpeted = listener.AssignedCallback.OnConnection( id, out connectionCallback );

            if ( connectionCallback != null )
            {
                RawConnection connection = new RawConnection();
                connection.AssignedID = id;
                connection.AssignedListenerID = listener.AssignedID;
                connection.AssignedSocket = listener.AssignedSocket;
                connection.AssignedCallback = connectionCallback;

                _connections.AddOrUpdate( id, connection, ( key, oldValue ) => connection );
                connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, new AsyncCallback( ReceiveCallback ), connection );
            }
        }

        private void ReceiveCallback( IAsyncResult result )
        {
            RawConnection connection = ( RawConnection )result.AsyncState;
            int bytesRead = connection.AssignedSocket.EndReceive( result );

            if ( bytesRead > 0 )
            {
                connection.AssignedCallback.OnData( connection.ReceiveBuffer, bytesRead, connection.AssignedID );
            }

            connection.AssignedSocket.BeginReceive( connection.ReceiveBuffer, new AsyncCallback( ReceiveCallback ), connection );
        }

        private void SendCallback( IAsyncResult result )
        {
            SendingContext context = ( SendingContext )result.AsyncState;
            RawConnection connection = context.Connection;
            connection.AssignedSocket.EndSend( result );
            context.FinishedEventHandler( connection.AssignedID );
        }

#pragma warning disable 67 // Not used, since named pipes don't work 
        public event DisconnectedHandler Disconnected;
#pragma warning restore 67
    }
}
